import re
import sys
import json
import toml
from pathlib import Path
from typing import Dict, List
from functools import lru_cache
import itertools

from plumbum import ProcessExecutionError, ProcessTimedOut, local
from loguru import logger


def file_text_replace(target: Path, old_text: str, new_text: str):
    with target.open("r") as f:
        content = f.read()

    content = content.replace(old_text, new_text)

    with target.open("w") as f:
        f.write(content)


def file_text_replace_by_pattern(target: Path, search_pattern: str, replace_pattern: str):
    with target.open("r") as f:
        content = f.read()

    content = re.sub(search_pattern, replace_pattern, content)

    with target.open("w") as f:
        f.write(content)


def setup_logger(log_folder: Path):
    log_folder.mkdir(exist_ok=True)

    logger.remove()
    logger.add(log_folder / "run.log", format="{time:YYYY-MM-DD HH:mm:ss} {level} {message}",
               backtrace=True, diagnose=True, rotation="1 day", level="DEBUG")
    logger.add(sys.stdout, format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> <level>{level} {message}</level>",
               level="DEBUG", colorize=True)


def exec_cmd(cmd: str, path: Path, env: Dict = {}, dismiss_error=False, timeout=None):
    logger.debug(f"Executing command: {cmd} ... ")
    pattern = r'\s+(?=(?:[^"]*"[^"]*")*[^"]*$)'
    sub_cmds = re.split(pattern, cmd)
    sub_cmds = [sub_cmd.replace('"', '')
                for sub_cmd in sub_cmds if sub_cmd != ""]
    tool = local[sub_cmds[0]]
    with local.cwd(path):
        try:
            with local.env(**env):
                if len(sub_cmds) == 1:
                    result = tool(timeout=timeout)
                else:
                    result = tool(sub_cmds[1:], timeout=timeout)
        except ProcessExecutionError as e:
            if dismiss_error:
                return e.stdout
            logger.exception(f"Executing Command({cmd}) failed!")
            sys.exit(1)
        except ProcessTimedOut as e:
            if dismiss_error:
                return "timeout"

            logger.exception(f"Executing Command({cmd}) timeout!")
            sys.exit(1)

    return result


def validate_project_compilability(path: Path) -> dict:
    '''尝试build验证效果，如果build成功就说明没问题'''
    logger.info(f"Validating project({path}) compilability ... ")
    result = {}
    build_errors = []

    build_msgs_raw = exec_cmd("cargo build -q --message-format=json",
                              path, env={"RUSTFLAGS": "-Awarnings"}, dismiss_error=True)

    build_msgs = [json.loads(line) for line in build_msgs_raw.splitlines()]

    for build_msg in build_msgs:
        if build_msg["reason"] == "compiler-artifact" or build_msg["reason"] == "build-script-executed":
            continue

        if build_msg["reason"] == "build-finished":
            result["success"] = build_msg["success"]

        if build_msg["reason"] == "compiler-message":
            msg = build_msg["message"]
            if msg["level"] == "error" and msg["code"] and msg["spans"]:
                build_error = {}
                build_error["code"] = msg["code"]["code"]
                build_error["label"] = msg["spans"][0]["label"]
                build_error["file"] = msg["spans"][0]["file_name"]
                build_error["line_start"] = msg["spans"][0]["line_start"]
                build_error["line_end"] = msg["spans"][0]["line_end"]
                build_error["col_start"] = msg["spans"][0]["column_start"]
                build_error["col_end"] = msg["spans"][0]["column_end"]
                build_error["text"] = msg["spans"][0]["text"]
                if msg["children"]:
                    build_error["error_detail"] = msg["children"][0]["message"]

                build_errors.append(build_error)
                logger.debug(f"Find build error: {build_error}")

    result["build_errors"] = build_errors

    try:
        if len(result) == 0 or result["success"]:
            logger.info(f"Project({path}) is compilable!")
        else:
            logger.info(
                f"Project({path}) is not compilable! Try to fix it ...")
    except Exception:
        logger.info(f"find error! build result:\n {result}")
        raise Exception("failed")

    return result


def handle_clippy_path_statement_with_no_effect(path: Path, messageDict: dict):
    '''处理Clippy中的path statement with no effect'''
    # logger.info(f"try to fix:  {messageDict.get("message")}")
    # print("msg", messageDict.get("spans"))
    file_name = messageDict.get("spans")[0].get("file_name")
    byte_start = messageDict.get("spans")[0].get("byte_start")
    byte_end = messageDict.get("spans")[0].get("byte_end")

    file_path = path / file_name

    with open(file_path, "rb") as file:
        content = file.read()

    replacement = b" " * (byte_end - byte_start)

    # 2. 删除指定范围的字节
    new_content = content[:byte_start] + replacement + content[byte_end:]

    # 3. 写回修改后的内容
    with open(file_path, "wb") as file:
        file.write(new_content)


def handle_help_try_statement_in_clippy(path: Path, try_replace_statement_collection: dict):
    '''有一些clippy推荐的写法，这里直接用上'''
    logger.info(f"try to fix try replace sugesstions")

    try_replace_statement_collection.sort(
        key=lambda x: (x['file_name'], -x['byte_start']))

    # 按建议逐文件替换代码
    for file_name, group in itertools.groupby(try_replace_statement_collection, key=lambda x: x['file_name']):
        
        # if(file_name == 'src/control_notify.rs'):
        #     for suggestion in group:
        #         print(suggestion)
        #         print(suggestion['rendered'])
        #         print()
        #     input()

        # 读取原始代码
        with open(path / file_name, 'rb') as file:
            content = file.read()

        start_pre = -1

        # 处理该文件中的所有建议（倒序）
        for suggestion in group:
            start = suggestion['byte_start']
            end = suggestion['byte_end']
            replacement: str = suggestion['suggested_replacement']

            if(start_pre == - 1 or end < start_pre):
                # 这个地方因为字节调整会导致字节坐标出现便宜，这里采用一些折中的方式，后续再改
                content = content[:start] + replacement.encode() + content[end:]
            
            start_pre = start

        # 保存修改后的代码
        with open(path / file_name, 'wb') as file:
            file.write(content)


def validate_project_using_clippy(path: Path) -> dict:
    '''尝试cargo clippy验证效果'''
    logger.info(
        f"Validating project({path}) compilability using check clippy ... ")
    result = {}
    build_errors = []

    build_msgs_raw = exec_cmd("cargo clippy -q --message-format=json",
                              path,
                              #   env={"RUSTFLAGS": "-Awarnings"},
                              dismiss_error=True
                              )

    build_msgs: list[dict] = [json.loads(line)
                              for line in build_msgs_raw.splitlines()]

    # 保留Cargo clippy建议替换的字段
    try_replace_statement_collection = []

    for build_msg in build_msgs:

        if (build_msg.get("message")
           and build_msg.get("message")
           and build_msg.get("message").get("message") == "path statement with no effect"):
            handle_clippy_path_statement_with_no_effect(
                path, build_msg.get("message"))

        elif (build_msg.get("message")
              and build_msg.get("message")
              and build_msg.get("message").get("children")
              and len(build_msg.get("message").get("children")) == 2
              and build_msg.get("message").get("children")[1].get("message") == "try"
              ):
            try:
                # print(build_msg.get("message"))
                try_replace_statement_collection.append(
                    {
                        'file_name': build_msg.get("message").get("children")[1].get("spans")[0].get("file_name"),
                        'byte_start': build_msg.get("message").get("children")[1].get("spans")[0].get("byte_start"),
                        'byte_end': build_msg.get("message").get("children")[1].get("spans")[0].get("byte_end"),
                        'suggested_replacement': build_msg.get("message").get("children")[1].get("spans")[0].get("suggested_replacement"),
                        'rendered': build_msg.get("message").get("rendered")
                    }
                )
            except:
                print(build_msg.get("message").get("children")[1].get("spans")[0])

    handle_help_try_statement_in_clippy(
        path,  try_replace_statement_collection)

    build_msgs_raw_again = exec_cmd("cargo clippy -q --message-format=json",
                                    path,
                                    env={"RUSTFLAGS": "-Awarnings"},
                                    dismiss_error=True
                                    )

    # print(build_msgs_raw_again)
    logger.info("clippy clean successfully!")
    # for build_msg in build_msgs:

    #     if(build_msg['message']):

    # if build_msg["reason"] == "compiler-artifact" or build_msg["reason"] == "build-script-executed":
    #     continue

    # if build_msg["reason"] == "build-finished":
    #     result["success"] = build_msg["success"]

    # if build_msg["reason"] == "compiler-message":
    #     msg = build_msg["message"]
    #     if msg["level"] == "error" and msg["code"] and msg["spans"]:
    #         build_error = {}
    #         build_error["code"] = msg["code"]["code"]
    #         build_error["label"] = msg["spans"][0]["label"]
    #         build_error["file"] = msg["spans"][0]["file_name"]
    #         build_error["line_start"] = msg["spans"][0]["line_start"]
    #         build_error["line_end"] = msg["spans"][0]["line_end"]
    #         build_error["col_start"] = msg["spans"][0]["column_start"]
    #         build_error["col_end"] = msg["spans"][0]["column_end"]
    #         build_error["text"] = msg["spans"][0]["text"]
    #         if msg["children"]:
    #             build_error["error_detail"] = msg["children"][0]["message"]

    #         build_errors.append(build_error)
    #         logger.debug(f"Find build error: {build_error}")

    # result["build_errors"] = build_errors

    # try:
    #     if len(result) == 0 or result["success"]:
    #         logger.info(f"Project({path}) is compilable!")
    #     else:
    #         logger.info(
    #             f"Project({path}) is not compilable! Try to fix it ...")
    # except Exception:
    #     logger.info(f"find error! build result:\n {result}")
    #     raise Exception("failed")

    return build_msgs_raw_again


def parse_cagro_build_errors(build_output) -> List:

    build_errors = []

    lines = build_output.splitlines()
    if not lines:
        logger.info(f"Build info is empty!")
        return build_errors

    logger.info(f"Parsing build info ... ")

    for idx, line in enumerate(lines):
        if idx == len(lines) - 1:
            break

        build_error = {}
        line = line.strip()

        error_info_line = lines[idx+1].strip()
        if line.startswith("error") and error_info_line.startswith("-->"):
            build_error["error"] = line
            build_error["file"], err_line_no, err_col_no = error_info_line[3:].strip(
            ).split(":")
            build_error["line_no"] = int(err_line_no)
            build_error["col_no"] = int(err_col_no)

            error_detail_line = lines[idx+4].lstrip("|").strip()
            match = re.search(r"\^+ (.*)$", error_detail_line)
            if match:
                build_error["error_detail"] = match.group(1)
            else:
                build_error["error_detail"] = ""

            build_errors.append(build_error)
            logger.debug(f"Find build error: {build_error}")

    return build_errors


@lru_cache(maxsize=1)
def get_configs(config_file: Path):
    logger.debug(f"Loading config file({config_file})")
    with config_file.open("r") as f:
        config = toml.load(f)
    logger.debug(f"Config: {config}")
    return config


if __name__ == "__main__":
    setup_logger(Path("/home/csslab/sandbox"))
    # res = exec_cmd("cargo build", Path("/home/csslab/c2rust/results/libarchive_231007_171144/P1_after_c2rust"),
    #                env={"RUSTFLAGS": "-Awarnings"}, dismiss_error=True)
    errors = validate_project_compilability(Path(
        "/home/csslab/c2rust/results/libarchive_231010_222013/P3_after_resolve_lifetime"))

    # logger.info(exec_cmd("ls -l", Path("results/libxml2-2.9_230924_161038/P3_after_resolve_lifetime/")))
